<?php
class DbQuery_MySQL implements IDbQuery {
    protected $conn;
    protected $prefix;
    protected $config = array (
        'type'   => null,
        'fields' => array(),
        'table'  => null,
        'join'   => array(),
        'order'  => array(),
        'where'  => array(),
        'values' => array(),
        'limit'  => array(
            'from'   => null,
            'offset' => null
        ),
        'group' => null
    );
    protected $compiled;

    public function __construct ($connection, $prefix) {
        $this->conn = $connection;
        $this->prefix = $prefix;
    }

    protected function getFirstArray ($args) {
        return ($args && is_array($args[0])) ? $args[0] : $args;
    }

    protected function checkType () {
        $this->validateType();
        $allowed = $this->getFirstArray(func_get_args());
        if ($allowed) {
            return in_array($this->config['type'], $allowed);
        }
    }

    protected function validateType () {
        if (empty($this->config['type'])) {
            throw new DatabaseException("Не указан тип запроса");
        }
        $allowed = $this->getFirstArray(func_get_args());
        if($allowed && !$this->checkType($allowed)) {
            throw new DatabaseException("Данный метод не разрешен для указанного типа запроса");
        }
    }

    /**
     * Возвращает текущий тип запроса
     * @return string
     */
    public function getType () {
        return $this->config['type'];
    }

    /**
     * @param string|array $fields
     * @return IDbQuery
     */
    public function select () {
        $this->config['type'] = 'select';
        return $this;
    }
    /**
     * @param string|array $fields
     * @return IDbQuery
     */
    public function insert () {
        $this->config['type'] = 'insert';
        return $this;
    }
    /**
     * @param string $inTable
     * @return IDbQuery
     */
    public function update () {
        $this->config['type'] = 'update';
        return $this;
    }
    /**
     * @param string $fromTable
     * @return IDbQuery
     */
    public function delete () {
        $this->config['type'] = 'delete';
        return $this;
    }

    protected function addPrefix ($tableName) {
        return empty($this->prefix) ? $tableName :
            $this->prefix . '_' . $tableName;
        return $this;
    }

    /**
     * Определяет таблицу
     * @param string $fromTable
     * @return IDbQuery
     */
    public function table ($table, $as = null) {
        $this->validateType();
        $as    = is_string($as) ? $as : $table;
        $table = $this->addPrefix($table);
        $this->config['table'] = $this->checkType('insert', 'delete') ?
            "`$table`" : "`$table` AS `$as`";
        return $this;
    }
    /**
     * С какими полями работать (применяется в insert, select)
     * @param array|string $fields
     */
    public function fields () {
        $this->validateType('select', 'insert');
        $fields = $this->getFirstArray(func_get_args());
        foreach ($fields as $f) {
            $this->config['fields'][] = $this->checkType('select') ? $f : "`$f`";
        }
        return $this;
    }

    protected function escape ($values) {
        if (is_array($values)) {
            foreach($values as $key => $value) {
                $values[$key] = $this->escape($value);
            }
            return $values;
        } else {
            return mysql_real_escape_string($values, $this->conn);
        }
    }
    /**
     * Какие значения вставить
     * @param string|array $values
     */
    public function values () {
        $this->validateType('insert');
        $values = $this->getFirstArray(func_get_args());
        foreach ($values as $key => $value) {
            $values[$key] = is_null($value) ? "NULL"
                : "'" . $this->escape($value) . "'";
        }
        $this->config['values'][] = $values;
        return $this;
    }

    /**
     * Устанавливает значение для update
     * @param string $value
     */
    public function set ($value) {
        $this->validateType('update');
        $args = func_get_args();
        array_shift($args);
        $args = $this->getFirstArray($args);
        $args = $this->escape($args);
        $this->config['set'][] = vsprintf($value, $args);
        return $this;
    }

    protected function join ($type, $table, $as, $on = null) {
        $as = is_null($on) ? $table : $as;
        $on = is_null($on) ? $as : $on;
        $this->validateType('select', 'update', 'delete');
        $table = $this->addPrefix($table);
        $this->config['join'][] = "$type JOIN `$table` AS `$as` \n  ON $on";
    }

    /**
     * INNER JOIN таблицы
     * @param string $table
     * @param string $on
     * @return IDbQuery
     */
    public function innerJoin ($table, $as, $on = null) {
        $this->join('INNER', $table, $as, $on);
        return $this;
    }
    /**
     * LEFT JOIN таблицы
     * @param string $table
     * @param string $on
     * @return IDbQuery
     */
    public function leftJoin  ($table, $as, $on = null) {
        $this->join('LEFT',  $table, $as, $on);
        return $this;
    }
    /**
     * RIGHT JOIN таблицы
     * @param string $table
     * @param string $on
     * @return IDbQuery
     */
    public function rightJoin ($table, $as, $on = null) {
        $this->join('RIGHT', $table, $as, $on);
        return $this;
    }

    /**
     * Порядок элементов
     * @param string $by
     * @param bool $desc
     * @return IDbQuery
     */
    public function order ($by, $desc = false) {
        $this->validateType('select');
        $this->config['order'][] = "$by " . ($desc ? "DESC" : "ASC");
        return $this;
    }

    protected function formWhere ($string, $args) {
        $this->validateType('select', 'update', 'delete');
        array_shift($args);
        $args = $this->getFirstArray($args);
        $args = $this->escape($args);
        $this->config['where'][] = vsprintf($string, $args);
        return $this;
        
    }
    /**
     * Условие поиска
     * @param string $cond
     * @return IDbQuery
     */
    public function where ($cond) {
        $this->validateType('select', 'update', 'delete');
        $this->formWhere("AND $cond", func_get_args());
        return $this;
    }
    /**
     * Условие поиска
     * @param string $cond
     * @return IDbQuery
     */
    public function orWhere ($cond) {
        $this->validateType('select', 'update', 'delete');
        $this->formWhere("OR $cond", func_get_args());
        return $this;
    }
    /**
     * Ограничение выборки
     * @param <type> $from
     * @param <type> $offset
     * @return IDbQuery
     */
    public function limit   ($from, $offset = null) {
        $this->validateType('select', 'update', 'delete');
        $this->config['limit']['offset'] = is_int($offset) ? $offset : $from;
        $this->config['limit']['from']   = is_int($offset) ? $from : null;
        return $this;
    }
    /**
     * Группировка по полю
     * @param string $field
     * @return IDbQuery
     */
    public function group ($field) {
        $this->validateType('select');
        $this->config['group'] = $field;
        return $this;
    }

    protected function important () {
        foreach (func_get_args() as $a) {
            if(empty($this->config[$a])) {
                throw new DatabaseException("Не заполнено: «{$a}»");
            }
        }
    }
    
    protected function compileSelect () {
        $this->important('table', 'fields');
        $fields = join (', ', $this->config['fields']);
        $table  = $this->config['table'];
        $joins  = join (" \n", $this->config['join']);
        $where  = join ("\n  ", $this->config['where']);
        // Отрежим AND/OR в начале
        $where  = "\nWHERE " . substr($where, strpos($where, ' ') + 1);
        $limitFrom   = $this->config['limit']['from'];
        $limitOffset = $this->config['limit']['offset'];
        $limit  = is_int($limitFrom) ? "LIMIT $limitFrom, $limitOffset" : '';
        $order  = join (",\n  ", $this->config['order']);
        $order  = $order ? "ORDER BY\n  $order \n" : '';
        $group  = $this->config['group'];
        $group  = $group ? "GROUP BY $group\n" : $group;
        return "SELECT $fields\nFROM $table \n" .
            "$joins $where\n$group$order$limit";
    }

    protected function compileInsert () {
        $this->important('table', 'fields', 'values');
        $table  = $this->config['table'];
        $fields = join (', ', $this->config['fields']);
        $valuesFull = $this->config['values'];
        foreach ($valuesFull as $key => $value) {
            $valuesFull[$key] = join(', ', $value);
        }
        $values= join (" ),\n( ", $valuesFull);
        return "INSERT INTO $table \n( $fields ) \nVALUES\n( $values )";
    }

    protected function compileUpdate () {
        $this->important('table', 'set');
        $table = $this->config['table'];
        //$limit = $this->config['limit']['offset'];
        //$limit = $limit ? "LIMIT $limit\n" : '';
        $joins = join (" \n", $this->config['join']);
        $joins = $joins ? "\n$joins" : '';
        $where = join ("\n  ", $this->config['where']);
        // Отрежим AND/OR в начале
        $where = "\nWHERE " . substr($where, strpos($where, ' ') + 1);
        $set   = join (",\n    ", $this->config['set']);
        return "UPDATE $table$joins \nSET $set $where"; // \n$limit";
    }
    
    protected function compileDelete () {
        $this->important('table', 'where');
        $table = $this->config['table'];
        $limit = $this->config['limit']['offset'];
        $limit = $limit ? "LIMIT $limit\n" : '';
        $joins = join (" \n", $this->config['join']);
        $joins = $joins ? "\n$joins" : '';
        $where = join ("\n  ", $this->config['where']);
        // Отрежим AND/OR в начале
        $where = "\nWHERE " . substr($where, strpos($where, ' ') + 1);
        return "DELETE FROM $table$joins $where \n$limit";

    }
    
    /**
     * "Приготовить" запрос
     * @return IDbQuery
     */
    public function compile  () {
        switch ($this->config['type']) {
            case "select" :
                $this->compiled = $this->compileSelect();
                break;
            case "insert" :
                $this->compiled = $this->compileInsert();
                break;
            case "update" :
                $this->compiled = $this->compileUpdate();
                break;
            case "delete" :
                $this->compiled = $this->compileDelete();
                break;
        }
        return $this;
    }
    /**
     * Получить "приготовленный" запрос
     * @return string
     */
    public function getQuery () {
        return $this->compiled;
    }
    /**
     * Выполнить "приготовленный" запрос
     * @return IDbQueryResult
     */
    public function run () {
        if ($this->compiled) {
            return new DbQueryResult_MySQL($this, $this->conn);
        } else {
            throw new DatabaseException("Запрос еще не скомпилен!");
        }
    }
}